package com.millstein.tsinglog.shiro.realm;

import com.millstein.tsinglog.commons.constant.SystemConstant;
import com.millstein.tsinglog.mapper.biz.UserMapper;
import com.millstein.tsinglog.utils.ConcurrentDateUtils;
import com.millstein.tsinglog.utils.IpUtils;
import com.millstein.tsinglog.utils.RequestUtils;
import com.millstein.tsinglog.model.pojo.Permission;
import com.millstein.tsinglog.model.pojo.User;
import com.millstein.tsinglog.result.ProfileResult;
import com.millstein.tsinglog.service.PermissionService;
import com.millstein.tsinglog.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@Slf4j
public class CustomRealm extends AuthorizingRealm {

    @Autowired
    private UserService userService;

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private PermissionService permissionService;

    @Autowired
    private RedisTemplate redisTemplate;

    // redis中记录用户登录次数的key的前缀
    private final String PREFIX_FOR_LOGIN_TIME = "login_time_";
    // redis中记录用户是否已经被锁的key的前缀
    private final String PREFIX_FOR_IS_LOCKED = "is_locked_";

    // 授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        // 1.获取安全数据
        ProfileResult result = (ProfileResult) principals.getPrimaryPrincipal();
        // 2.获取权限信息
        Set<Permission> set = permissionService.getPermissionByUsername(result.getUsername());
        Set<String> pids = set.stream().map(Permission::getPermissionCode).collect(Collectors.toSet());
        // 3.构造安全数据并返回
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.setStringPermissions(pids);
        return info;
    }

    // 认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        UsernamePasswordToken upToken = (UsernamePasswordToken) token;

        String username = upToken.getUsername();
        String password = new String(upToken.getPassword());

        // 从redis中拿到这个用户密码输入错误的次数
        Integer times = (Integer) redisTemplate.opsForValue().get(PREFIX_FOR_LOGIN_TIME + username);
        if (times == null) { // 如果没有这个值，那么说明这个用户前一次登录的时候密码输入正确
            times = 0;
        }

        Map<String, String> logMap = new HashMap<>();

        User dbUser = userService.getUserByUsername(username);

        // 1.检查用户名是否存在
        if (dbUser == null) {
            logMap.put("success", "false");
            logMap.put("reason", "用户名不存在");
            this.doLog(username, logMap);
            throw new UnknownAccountException("用户名不存在");
        }

        // 2.检查用户是否由于重试次数过多而暂时锁定
        String isLocked = (String) redisTemplate.opsForValue().get(PREFIX_FOR_IS_LOCKED + username);
        if (StringUtils.isNotBlank(isLocked)) {
            logMap.put("success", "false");
            logMap.put("reason", "账户已经被锁定");
            this.doLog(username, logMap);
            throw new LockedAccountException("密码输入错误次数过多，账户已被锁定");
        }

        // 3.检查用户是否被管理员锁定了账户
        if (dbUser.getUsable() == SystemConstant.USABLE_ENTITY_OFF) {

            logMap.put("success", "false");
            logMap.put("reason", "账户已经被管理员锁定");
            this.doLog(username, logMap);
            throw new DisabledAccountException("账户被锁，请联系管理员");
        }

        // 4.检查密码是否正确
        if (!dbUser.getPassword().equals(password)) { // 密码错误

            if (times >= 5) {
                // 锁定账户，时长一个小时
                redisTemplate.opsForValue().set(PREFIX_FOR_IS_LOCKED + username, "LOCKED", 60, TimeUnit.MINUTES);
                // 删除登录次数的记录
                redisTemplate.delete(PREFIX_FOR_LOGIN_TIME + username);

                logMap.put("success", "false");
                logMap.put("reason", "密码错误，密码连续输入错误已达到5次，账户锁定");
                this.doLog(username, logMap);
            } else {
                times++;
                redisTemplate.opsForValue().set(PREFIX_FOR_LOGIN_TIME + username, times);

                logMap.put("success", "false");
                logMap.put("reason", "密码错误，该账户密码连续输入错误已达到" + times + "次");
                this.doLog(username, logMap);
            }

            throw new IncorrectCredentialsException("密码有误");
        } else { // 密码正确
            // 删除登录次数的记录
            redisTemplate.delete(PREFIX_FOR_LOGIN_TIME + username);
            logMap.put("success", "true");
            this.doLog(username, logMap);
        }

        // 修改用户的最后登录信息
        dbUser.setLastLoginIp(IpUtils.getRealIp(RequestUtils.getRequest()));
        Date date = new Date();
        String dateStr = ConcurrentDateUtils.format(date);
        dbUser.setLastLoginTime(dateStr);
        dbUser.setUpdateTime(dateStr);
        dbUser.setLoginCount(dbUser.getLoginCount() + 1);
        userMapper.updateByPrimaryKey(dbUser);

        List<Permission> list = permissionService.getPermissionByRole(dbUser.getRoleId(), true);

        ProfileResult profileResult = new ProfileResult(dbUser, list, userService.getUserByCode("comment:check"));

        return new SimpleAuthenticationInfo(profileResult, password, super.getName());
    }


    /**
     * 日志打印
     * @param username 用户名
     * @param logMap 认证结果
     * @throws Exception
     */
    private void doLog(String username, Map<String, String> logMap) {

        String result = logMap.get("success");
        String reason = logMap.get("reason");

        if (Boolean.parseBoolean(result)) {
            log.info("来自ip为[{}]的登录，登录账号[{}]，登录结果：[{}]", RequestUtils.getIp(), username, result);
        } else {
            log.info("来自ip为[{}]的登录，登录账号[{}]，登录结果：[{}]，原因：[{}]", RequestUtils.getIp(),
                    username, result, reason);
        }

    }

}
